package omega

import (
	"context"
	"errors"
	"fmt"
	"reflect"
)

var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
var errorType = reflect.TypeOf((*error)(nil)).Elem()

func createErrOut(targetFunc reflect.Value, err reflect.Value) []reflect.Value {
	out := make([]reflect.Value, targetFunc.Type().NumOut())
	last := targetFunc.Type().NumOut() - 1
	for i := 0; i < last; i++ {
		out[i] = reflect.Zero(targetFunc.Type().Out(i))
	}
	out[last] = err
	return out
}

// Decorate injects two functions(injectedBefore & injectedAfter) into the target function.
// The argument decorated is the function after decoration.
// The argument target is the function to be decorated.
// The argument before is the function to be injected before the target function.
// The argument after is the function to be injected after the target function.
func decorate(decorated interface{}, target interface{}, before interface{}, after interface{}) (err error) {
	var targetFunc reflect.Value
	var decoratedFunc reflect.Value
	var beforeFunc reflect.Value
	var afterFunc reflect.Value

	decoratedFunc, err = checkDecorator(decorated)
	if err != nil {
		return
	}

	targetFunc, err = checkTarget(target)
	if err != nil {
		return
	}

	beforeFunc, afterFunc, err = checkInjection(before, after)
	if err != nil {
		return
	}
	decoratedFunc.Set(reflect.MakeFunc(targetFunc.Type(), func(in []reflect.Value) (out []reflect.Value) {
		var ctx reflect.Value
		var err error
		if targetFunc.Type().IsVariadic() {
			if before != nil {
				beforeOut := beforeFunc.CallSlice(in)
				if !beforeOut[len(beforeOut)-1].IsNil() {
					out = createErrOut(targetFunc, beforeOut[len(beforeOut)-1])
					return
				}
				ctx = beforeOut[0]
			}
			in[0] = ctx
			out, err = safeCallSlice(targetFunc, in)
			if after != nil {
				afterOut := afterFunc.CallSlice([]reflect.Value{ctx, reflect.ValueOf(err)})
				if !afterOut[len(afterOut)-1].IsNil() {
					out = createErrOut(targetFunc, afterOut[len(afterOut)-1])
					return
				}
			}
		} else {
			if before != nil {
				beforeOut := beforeFunc.Call([]reflect.Value{in[0], reflect.ValueOf(in[1:])})
				if !beforeOut[len(beforeOut)-1].IsNil() {
					out = createErrOut(targetFunc, beforeOut[len(beforeOut)-1])
					return
				}
				ctx = beforeOut[0]
			}
			in[0] = ctx
			out, err = safeCall(targetFunc, in)
			if after != nil {
				afterOut := afterFunc.Call([]reflect.Value{ctx, out[len(out)-1]})
				if !afterOut[len(afterOut)-1].IsNil() {
					out = createErrOut(targetFunc, afterOut[len(afterOut)-1])
					return
				}
			}
		}
		return
	}))
	return
}

func safeCallSlice(targetFunc reflect.Value, in []reflect.Value) (out []reflect.Value, err error) {
	defer func() {
		cause := recover()
		switch cause.(type) {
		case error:
			err = cause.(error)
		case string:
			err = errors.New(cause.(string))
		}
	}()
	out = targetFunc.CallSlice(in)
	if len(out) > 0 && out[len(out)-1].Type().Implements(errorType) {
		if out[len(out)-1].Interface() != nil {
			err = out[len(out)-1].Interface().(error)
		}
	}
	return
}

func safeCall(targetFunc reflect.Value, in []reflect.Value) (out []reflect.Value, err error) {
	defer func() {
		if cause := recover(); cause != nil {
			switch cause.(type) {
			case error:
				err = cause.(error)
				out = createErrOut(targetFunc, reflect.ValueOf(err))
			case string:
				err = errors.New(cause.(string))
				out = createErrOut(targetFunc, reflect.ValueOf(err))
			}
		}
	}()
	out = targetFunc.Call(in)
	if len(out) > 0 && out[len(out)-1].Type().Implements(errorType) {
		if out[len(out)-1].Interface() != nil {
			err = out[len(out)-1].Interface().(error)
		}
	}
	return
}

func checkFPTR(fptr interface{}) (function reflect.Value, err error) {
	if fptr == nil {
		err = fmt.Errorf("Input para is nil.")
		return
	}
	if reflect.TypeOf(fptr).Kind() != reflect.Ptr {
		err = fmt.Errorf("Input para is not a pointer.")
		return
	}
	function = reflect.ValueOf(fptr).Elem()
	if function.Kind() != reflect.Func {
		err = fmt.Errorf("Input para is not a pointer to a function.")
		return
	}

	return
}

func checkInjection(before interface{}, after interface{}) (beforeFunc reflect.Value, afterFunc reflect.Value, err error) {
	if before != nil {
		beforeFunc = reflect.ValueOf(before)
		if beforeFunc.Kind() != reflect.Func {
			err = fmt.Errorf("Only a function can be injected before.")
			return
		}
		if beforeFunc.Type().NumIn() == 0 {
			err = fmt.Errorf("The input para number of the function injected before must contain context.Context.")
			return
		}
		if !beforeFunc.Type().In(0).Implements(contextType) {
			err = fmt.Errorf("The first input para type of the function injected before must be context.Context.")
			return
		}
		if beforeFunc.Type().NumOut() != 2 {
			err = fmt.Errorf("The output para number of the function injected before must be two.")
			return
		}
		if !beforeFunc.Type().Out(0).Implements(contextType) {
			err = fmt.Errorf("The output para type of the function injected before must be context.Context.")
			return
		}
		if !beforeFunc.Type().Out(1).Implements(errorType) {
			err = fmt.Errorf("The second output para type of the function injected before must be error.")
			return
		}
	}
	if after != nil {
		afterFunc = reflect.ValueOf(after)
		if afterFunc.Kind() != reflect.Func {
			err = fmt.Errorf("Only a function can be injected after.")
			return
		}
		if beforeFunc.Type().NumIn() == 0 {
			err = fmt.Errorf("The input para number of the function injected before must contain context.Context.")
			return
		}
		if !beforeFunc.Type().In(0).Implements(contextType) {
			err = fmt.Errorf("The first input para types of the function injected after must be context.Context.")
			return
		}
		if afterFunc.Type().NumOut() != 1 {
			err = fmt.Errorf("The output para number of the function injected after must be one.")
			return
		}
		if !afterFunc.Type().Out(0).Implements(errorType) {
			err = fmt.Errorf("The output para type of the function injected after must be error.")
			return
		}
	}
	return
}

func checkDecorator(decorator interface{}) (decoFunc reflect.Value, err error) {
	decoFunc, err = checkFPTR(decorator)
	if err != nil {
		return
	}
	if decoFunc.Type().NumIn() == 0 {
		err = fmt.Errorf("The input para number of the decorator function must contain context.Context.")
		return
	}
	if !decoFunc.Type().In(0).Implements(contextType) {
		err = fmt.Errorf("The first input para types of the decorator function must be context.Context.")
		return
	}

	if !decoFunc.Type().Out(decoFunc.Type().NumOut() - 1).Implements(errorType) {
		err = fmt.Errorf("The last out para number of the decoFunc function must be error")
		return
	}
	return
}

func checkTarget(target interface{}) (targetFunc reflect.Value, err error) {
	targetFunc = reflect.ValueOf(target)
	if targetFunc.Kind() != reflect.Func {
		err = fmt.Errorf("target mast be  a function.")
		return
	}
	if targetFunc.Type().NumIn() == 0 {
		err = fmt.Errorf("The input para number of the target function must contain context.Context.")
		return
	}
	if !targetFunc.Type().In(0).Implements(contextType) {
		err = fmt.Errorf("The first input para number of the target function must contain context.Context.")
		return
	}
	if !targetFunc.Type().Out(targetFunc.Type().NumOut() - 1).Implements(errorType) {
		err = fmt.Errorf("The last out para number of the target function must be error")
		return
	}
	return targetFunc, nil
}
